import {
  slice,
  pluck,
  each,
  bind,
  create,
  isList,
  isFunction,
  isObject
} from "@/libs/utils";
import * as allStorages from "./storages";

const storeAPI = {
  version: "2.0.12",
  enabled: false,

  // get returns the value of the given key. If that value
  // is undefined, it returns optionalDefaultValue instead.
  get: function(key, optionalDefaultValue) {
    var data = this.storage.read(this._namespacePrefix + key);
    return this._deserialize(data, optionalDefaultValue);
  },

  // set will store the given value at key and returns value.
  // Calling set with value === undefined is equivalent to calling remove.
  set: function(key, value) {
    if (value === undefined) {
      return this.remove(key);
    }
    this.storage.write(this._namespacePrefix + key, this._serialize(value));
    return value;
  },

  // remove deletes the key and value stored at the given key.
  remove: function(key) {
    this.storage.remove(this._namespacePrefix + key);
  },

  // each will call the given callback once for each key-value pair
  // in this store.
  each: function(callback) {
    var self = this;
    this.storage.each(function(val, namespacedKey) {
      callback.call(
        self,
        self._deserialize(val),
        (namespacedKey || "").replace(self._namespaceRegexp, "")
      );
    });
  },

  // clearAll will remove all the stored key-value pairs in this store.
  clearAll: function() {
    this.storage.clearAll();
  },

  // additional functionality that can't live in plugins
  // ---------------------------------------------------

  // hasNamespace returns true if this store instance has the given namespace.
  hasNamespace: function(namespace) {
    return this._namespacePrefix == "__dushu_" + namespace + "_";
  },

  // createStore creates a store.js instance with the first
  // functioning storage in the list of storage candidates,
  // and applies the the given mixins to the instance.
  createStore: function() {
    return createStore.apply(this, arguments);
  },

  addPlugin: function(plugin) {
    this._addPlugin(plugin);
  },

  namespace: function(namespace) {
    return createStore(this.storage, this.plugins, namespace);
  }
};

function _warn() {
  var _console = typeof console == "undefined" ? null : console;
  if (!_console) {
    return;
  }
  var fn = _console.warn ? _console.warn : _console.log;
  fn.apply(_console, arguments);
}

function createStore(storages, plugins, namespace) {
  if (!namespace) {
    namespace = "";
  }
  if (storages && !isList(storages)) {
    storages = [storages];
  }
  if (plugins && !isList(plugins)) {
    plugins = [plugins];
  }

  var namespacePrefix = namespace ? "__dushu_" + namespace + "_" : "";
  var namespaceRegexp = namespace ? new RegExp("^" + namespacePrefix) : null;

  if (!/^[a-zA-Z0-9_-]*$/.test(namespace)) {
    throw new Error(
      "store.js namespaces can only have alphanumerics + underscores and dashes"
    );
  }

  var _privateStoreProps = {
    _namespacePrefix: namespacePrefix,
    _namespaceRegexp: namespaceRegexp,

    _testStorage: function(storage) {
      try {
        var testStr = "__dushu__test__";
        storage.write(testStr, testStr);
        var ok = storage.read(testStr) === testStr;
        storage.remove(testStr);
        return ok;
      } catch (e) {
        return false;
      }
    },

    _assignPluginFnProp: function(pluginFnProp, propName) {
      var oldFn = this[propName];
      this[propName] = function pluginFn() {
        var args = slice(arguments, 0);
        var self = this;

        // super_fn calls the old function which was overwritten by
        // this mixin.
        function super_fn() {
          if (!oldFn) {
            return;
          }
          each(arguments, function(arg, i) {
            args[i] = arg;
          });
          return oldFn.apply(self, args);
        }

        // Give mixing function access to super_fn by prefixing all mixin function
        // arguments with super_fn.
        var newFnArgs = [super_fn].concat(args);

        return pluginFnProp.apply(self, newFnArgs);
      };
    },

    _serialize: function(obj) {
      return JSON.stringify(obj);
    },

    _deserialize: function(strVal, defaultVal) {
      if (!strVal) {
        return defaultVal;
      }
      // It is possible that a raw string value has been previously stored
      // in a storage without using store.js, meaning it will be a raw
      // string value instead of a JSON serialized string. By defaulting
      // to the raw string value in case of a JSON parse error, we allow
      // for past stored values to be forwards-compatible with store.js
      var val = "";
      try {
        val = JSON.parse(strVal);
      } catch (e) {
        val = strVal;
      }

      return val !== undefined ? val : defaultVal;
    },

    _addStorage: function(storage) {
      if (this.enabled) {
        return;
      }
      if (this._testStorage(storage)) {
        this.storage = storage;
        this.enabled = true;
      }
    },

    _addPlugin: function(plugin) {
      var self = this;

      // If the plugin is an array, then add all plugins in the array.
      // This allows for a plugin to depend on other plugins.
      if (isList(plugin)) {
        each(plugin, function(plugin) {
          self._addPlugin(plugin);
        });
        return;
      }

      // Keep track of all plugins we've seen so far, so that we
      // don't add any of them twice.
      var seenPlugin = pluck(this.plugins, function(seenPlugin) {
        return plugin === seenPlugin;
      });
      if (seenPlugin) {
        return;
      }
      this.plugins.push(plugin);

      // Check that the plugin is properly formed
      if (!isFunction(plugin)) {
        throw new Error("Plugins must be function values that return objects");
      }

      var pluginProperties = plugin.call(this);
      if (!isObject(pluginProperties)) {
        throw new Error("Plugins must return an object of function properties");
      }

      // Add the plugin function properties to this store instance.
      each(pluginProperties, function(pluginFnProp, propName) {
        if (!isFunction(pluginFnProp)) {
          throw new Error(
            "Bad plugin property: " +
              propName +
              " from plugin " +
              plugin.name +
              ". Plugins should only return functions."
          );
        }
        self._assignPluginFnProp(pluginFnProp, propName);
      });
    },

    // Put deprecated properties in the private API, so as to not expose it to accidential
    // discovery through inspection of the store object.

    // Deprecated: addStorage
    addStorage: function(storage) {
      _warn(
        "store.addStorage(storage) is deprecated. Use createStore([storages])"
      );
      this._addStorage(storage);
    }
  };

  var store = create(_privateStoreProps, storeAPI, {
    plugins: []
  });
  store.raw = {};
  each(store, function(prop, propName) {
    if (isFunction(prop)) {
      store.raw[propName] = bind(store, prop);
    }
  });
  each(storages, function(storage) {
    store._addStorage(storage);
  });
  each(plugins, function(plugin) {
    store._addPlugin(plugin);
  });
  return store;
}

const storeConstruct = {
  inited: false,
  createStore,
  localStorage: createStore(allStorages.localStorage),
  sessionStorage: createStore(allStorages.sessionStorage),
  cookieStorage: createStore(allStorages.cookieStorage),
  memoryStorage: createStore(allStorages.memoryStorage),
  initStores: (Vue, options = {}) => {
    if (storeConstruct.inited) {
      return;
    }

    const namespace = options.namespace || "dushu";
    const plugins = options.plugins || [];

    storeConstruct.inited = true;

    Object.keys(allStorages).forEach(key => {
      const storage = allStorages[key];
      const Store = createStore(storage, plugins, namespace);
      storeConstruct[storage.name] = Store;

      if (Vue) {
        Vue.prototype[`$${storage.name}`] = Store;
      }
    });
  }
};

export default storeConstruct;
